/*
 * Copyright 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.code.guidatv.client.ui.widget.imported;

import java.util.Iterator;

import com.google.gwt.animation.client.Animation;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasCloseHandlers;
import com.google.gwt.event.logical.shared.HasOpenHandlers;
import com.google.gwt.event.logical.shared.HasResizeHandlers;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DisclosureEvent;
import com.google.gwt.user.client.ui.DisclosureHandler;
import com.google.gwt.user.client.ui.DisclosurePanelImages;
import com.google.gwt.user.client.ui.FiresDisclosureEvents;
import com.google.gwt.user.client.ui.HasAnimation;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * A widget that consists of a header and a content panel that discloses the
 * content when a user clicks on the header.
 *
 * <h3>CSS Style Rules</h3>
 * <dl class="css">
 * <dt>.gwt-DisclosurePanel
 * <dd>the panel's primary style
 * <dt>.gwt-DisclosurePanel-open
 * <dd>dependent style set when panel is open
 * <dt>.gwt-DisclosurePanel-closed
 * <dd>dependent style set when panel is closed
 * </dl>
 * <p>
 * <img class='gallery' src='doc-files/DisclosurePanel.png'/>
 * </p>
 *
 * <p>
 * The header and content sections can be easily selected using css with a child
 * selector:<br/>
 * .gwt-DisclosurePanel-open .header { ... }
 * </p>
 * <h3>Use in UiBinder Templates</h3>
 * <p>
 * DisclosurePanel elements in {@link com.google.gwt.uibinder.client.UiBinder
 * UiBinder} templates can have one widget child and one of two types of header
 * elements. A &lt;g:header> element can hold text (not html), or a
 * &lt;g:customHeader> element can hold a widget. (Note that the tags of the
 * header elements are not capitalized. This is meant to signal that the header
 * is not a runtime object, and so cannot have a <code>ui:field</code>
 * attribute.)
 * <p>
 * For example:
 *
 * <pre>
 * &lt;g:DisclosurePanel>
 *   &lt;g:header>Text header&lt;/g:header>
 *   &lt;g:Label>Widget body&lt;/g:Label>
 * &lt;/g:DisclosurePanel>
 *
 * &lt;g:DisclosurePanel>
 *   &lt;g:customHeader>
 *     &lt;g:Label>Widget header&lt;/g:Label>
 *   &lt;/g:customHeader>
 *   &lt;g:Label>Widget body&lt;/g:Label>
 * &lt;/g:DisclosurePanel>
 * </pre>
 */
@SuppressWarnings("deprecation")
public final class DisclosurePanel extends Composite implements
        FiresDisclosureEvents, HasWidgets, HasAnimation,
        HasOpenHandlers<DisclosurePanel>, HasCloseHandlers<DisclosurePanel>,
        HasResizeHandlers {
    interface DefaultImages extends ClientBundle {
        @ImageOptions(flipRtl = true)
        ImageResource disclosurePanelClosed();

        ImageResource disclosurePanelOpen();
    }

    private static final DefaultImages DEFAULT_IMAGES = GWT
            .create(DefaultImages.class);

    /**
     * Used to wrap widgets in the header to provide click support. Effectively
     * wraps the widget in an <code>anchor</code> to get automatic keyboard
     * access.
     */
    private final class ClickableHeader extends SimplePanel {

        private ClickableHeader() {
            // Anchor is used to allow keyboard access.
            super(DOM.createAnchor());
            Element elem = getElement();
            DOM.setElementProperty(elem, "href", "javascript:void(0);");
            // Avoids layout problems from having blocks in inlines.
            DOM.setStyleAttribute(elem, "display", "block");
            sinkEvents(Event.ONCLICK);
            setStyleName(STYLENAME_HEADER);
        }

        @Override
        public void onBrowserEvent(Event event) {
            // no need to call super.
            switch (DOM.eventGetType(event)) {
            case Event.ONCLICK:
                // Prevent link default action.
                DOM.eventPreventDefault(event);
                setOpen(!isOpen);
            }
        }
    }

    /**
     * An {@link Animation} used to open the content.
     */
    private static class ContentAnimation extends Animation {
        /**
         * Whether the item is being opened or closed.
         */
        private boolean opening;

        /**
         * The {@link DisclosurePanel} being affected.
         */
        private DisclosurePanel curPanel;

        /**
         * Open or close the content.
         *
         * @param panel the panel to open or close
         * @param animate true to animate, false to open instantly
         */
        public void setOpen(DisclosurePanel panel, boolean animate) {
            // Immediately complete previous open
            cancel();

            // Open the new item
            if (animate) {
                curPanel = panel;
                opening = panel.isOpen;
                run(ANIMATION_DURATION);
            } else {
                panel.contentWrapper.setVisible(panel.isOpen);
                if (panel.isOpen) {
                    // Special treatment on the visible case to ensure LazyPanel
                    // works
                    panel.getContent().setVisible(true);
                }
            }
        }

        @Override
        protected void onComplete() {
            if (!opening) {
                curPanel.contentWrapper.setVisible(false);
            }
            DOM.setStyleAttribute(curPanel.contentWrapper.getElement(),
                    "height", "auto");
            ResizeEvent.fire(curPanel, curPanel.getOffsetWidth(), curPanel.getOffsetHeight());
            curPanel = null;
        }

        @Override
        protected void onStart() {
            super.onStart();
            if (opening) {
                curPanel.contentWrapper.setVisible(true);
                // Special treatment on the visible case to ensure LazyPanel
                // works
                curPanel.getContent().setVisible(true);
            }
        }

        @Override
        protected void onUpdate(double progress) {
            int scrollHeight = DOM.getElementPropertyInt(
                    curPanel.contentWrapper.getElement(), "scrollHeight");
            int height = (int) (progress * scrollHeight);
            if (!opening) {
                height = scrollHeight - height;
            }
            height = Math.max(height, 1);
            DOM.setStyleAttribute(curPanel.contentWrapper.getElement(),
                    "height", height + "px");
            DOM.setStyleAttribute(curPanel.contentWrapper.getElement(),
                    "width", "auto");
            ResizeEvent.fire(curPanel, curPanel.getOffsetWidth(), curPanel.getOffsetHeight());
        }
    }

    /**
     * The default header widget used within a {@link DisclosurePanel}.
     */
    private class DefaultHeader extends Widget implements HasText,
            OpenHandler<DisclosurePanel>, CloseHandler<DisclosurePanel> {

        /**
         * imageTD holds the image for the icon, not null. labelTD holds the
         * text for the label.
         */
        private final Element labelTD;

        private final Image iconImage;
        private final Imager imager;

        private DefaultHeader(final DisclosurePanelImages images, String text) {
            this(new Imager() {
                public Image makeImage() {
                    return images.disclosurePanelClosed().createImage();
                }

                public void updateImage(boolean open, Image image) {
                    if (open) {
                        images.disclosurePanelOpen().applyTo(image);
                    } else {
                        images.disclosurePanelClosed().applyTo(image);
                    }
                }
            }, text);
        }

        private DefaultHeader(Imager imager, String text) {
            this.imager = imager;
            iconImage = imager.makeImage();

            // I do not need any Widgets here, just a DOM structure.
            Element root = DOM.createTable();
            Element tbody = DOM.createTBody();
            Element tr = DOM.createTR();
            final Element imageTD = DOM.createTD();
            labelTD = DOM.createTD();

            setElement(root);

            DOM.appendChild(root, tbody);
            DOM.appendChild(tbody, tr);
            DOM.appendChild(tr, imageTD);
            DOM.appendChild(tr, labelTD);

            // set image TD to be same width as image.
            DOM.setElementProperty(imageTD, "align", "center");
            DOM.setElementProperty(imageTD, "valign", "middle");
            DOM.setStyleAttribute(imageTD, "width", iconImage.getWidth() + "px");

            DOM.appendChild(imageTD, iconImage.getElement());

            setText(text);

            addOpenHandler(this);
            addCloseHandler(this);
            setStyle();
        }

        private DefaultHeader(final ImageResource openImage,
                final ImageResource closedImage, String text) {
            this(new Imager() {
                public Image makeImage() {
                    return new Image(closedImage);
                }

                public void updateImage(boolean open, Image image) {
                    if (open) {
                        image.setResource(openImage);
                    } else {
                        image.setResource(closedImage);
                    }
                }
            }, text);
        }

        public final String getText() {
            return DOM.getInnerText(labelTD);
        }

        public final void onClose(CloseEvent<DisclosurePanel> event) {
            setStyle();
        }

        public final void onOpen(OpenEvent<DisclosurePanel> event) {
            setStyle();
        }

        public final void setText(String text) {
            DOM.setInnerText(labelTD, text);
        }

        private void setStyle() {
            imager.updateImage(isOpen, iconImage);
        }
    }

    private interface Imager {
        Image makeImage();

        void updateImage(boolean open, Image image);
    }

    /**
     * The duration of the animation.
     */
    private static final int ANIMATION_DURATION = 350;

    // Stylename constants.
    private static final String STYLENAME_DEFAULT = "gwt-DisclosurePanel";

    private static final String STYLENAME_SUFFIX_OPEN = "open";

    private static final String STYLENAME_SUFFIX_CLOSED = "closed";

    private static final String STYLENAME_HEADER = "header";

    private static final String STYLENAME_CONTENT = "content";

    /**
     * The {@link Animation} used to open and close the content.
     */
    private static ContentAnimation contentAnimation;

    /**
     * top level widget. The first child will be a reference to {@link #header}.
     * The second child will be a reference to {@link #contentWrapper}.
     */
    private final VerticalPanel mainPanel = new VerticalPanel();

    /**
     * The wrapper around the content widget.
     */
    private final SimplePanel contentWrapper = new SimplePanel();

    /**
     * holds the header widget.
     */
    private final ClickableHeader header = new ClickableHeader();

    private boolean isAnimationEnabled = false;

    private boolean isOpen = false;

    /**
     * Creates an empty DisclosurePanel that is initially closed.
     */
    public DisclosurePanel() {
        initWidget(mainPanel);
        mainPanel.add(header);
        mainPanel.add(contentWrapper);
        DOM.setStyleAttribute(contentWrapper.getElement(), "padding", "0px");
        DOM.setStyleAttribute(contentWrapper.getElement(), "overflow", "hidden");
        setStyleName(STYLENAME_DEFAULT);
        setContentDisplay(false);
    }

    /**
     * Creates a DisclosurePanel with the specified header text, an initial
     * open/close state and a bundle of images to be used in the default header
     * widget.
     *
     * @param images a bundle that provides disclosure panel specific images
     * @param headerText the text to be displayed in the header
     * @param isOpen the initial open/close state of the content panel
     *
     * @deprecated use
     * {@link #DisclosurePanel(ImageResource, ImageResource, String)} and
     * {@link #setOpen(boolean)}
     */
    @Deprecated
    public DisclosurePanel(DisclosurePanelImages images, String headerText,
            boolean isOpen) {
        this();
        setOpen(isOpen);
        setHeader(new DefaultHeader(images, headerText));
    }

    /**
     * Creates a DisclosurePanel with the specified header text, an initial
     * open/close state and a bundle of images to be used in the default header
     * widget.
     *
     * @param openImage the open state image resource
     * @param closedImage the closed state image resource
     * @param headerText the text to be displayed in the header
     */
    public DisclosurePanel(ImageResource openImage, ImageResource closedImage,
            String headerText) {
        this();
        setHeader(new DefaultHeader(openImage, closedImage, headerText));
    }

    /**
     * Creates a DisclosurePanel that will be initially closed using the
     * specified text in the header.
     *
     * @param headerText the text to be displayed in the header
     */
    public DisclosurePanel(String headerText) {
        this(DEFAULT_IMAGES.disclosurePanelOpen(), DEFAULT_IMAGES
                .disclosurePanelClosed(), headerText);
    }

    /**
     * Creates a DisclosurePanel with the specified header text and an initial
     * open/close state.
     *
     * @param headerText the text to be displayed in the header
     * @param isOpen the initial open/close state of the content panel
     * @deprecated use {@link #DisclosurePanel(String)} and
     * {@link #setOpen(boolean)}
     */
    @Deprecated
    public DisclosurePanel(String headerText, boolean isOpen) {
        this(DEFAULT_IMAGES.disclosurePanelOpen(), DEFAULT_IMAGES
                .disclosurePanelClosed(), headerText);
        this.setOpen(isOpen);
    }

    /**
     * Creates a DisclosurePanel that will be initially closed using a widget as
     * the header.
     *
     * @param header the widget to be used as a header
     * @deprecated use {@link #DisclosurePanel()} and {@link #setHeader(Widget)}
     */
    public DisclosurePanel(Widget header) {
        this();
        setHeader(header);
    }

    /**
     * Creates a DisclosurePanel using a widget as the header and an initial
     * open/close state.
     *
     * @param header the widget to be used as a header
     * @param isOpen the initial open/close state of the content panel
     * @deprecated use {@link #DisclosurePanel()}, {@link #setOpen(boolean)} and
     * {@link #setHeader(Widget)} instead
     */
    @Deprecated
    public DisclosurePanel(Widget header, boolean isOpen) {
        this();
        setHeader(header);
        setOpen(isOpen);
    }

    public void setHeaderText(String headerText) {
        setHeader(new DefaultHeader(DEFAULT_IMAGES.disclosurePanelOpen(),
                DEFAULT_IMAGES.disclosurePanelClosed(), headerText));
    }

    public void add(Widget w) {
        if (this.getContent() == null) {
            setContent(w);
        } else {
            throw new IllegalStateException(
                    "A DisclosurePanel can only contain two Widgets.");
        }
    }

    public HandlerRegistration addCloseHandler(
            CloseHandler<DisclosurePanel> handler) {
        return addHandler(handler, CloseEvent.getType());
    }

    /**
     * Attaches an event handler to the panel to receive {@link DisclosureEvent}
     * notification.
     *
     * @param handler the handler to be added (should not be null)
     * @deprecated Use {@link DisclosurePanel#addOpenHandler(OpenHandler)} and
     * {@link DisclosurePanel#addCloseHandler(CloseHandler)} instead
     */
    @Deprecated
    public void addEventHandler(final DisclosureHandler handler) {
    }

    public HandlerRegistration addOpenHandler(
            OpenHandler<DisclosurePanel> handler) {
        return addHandler(handler, OpenEvent.getType());
    }

    @Override
    public HandlerRegistration addResizeHandler(ResizeHandler handler) {
        return addHandler(handler, ResizeEvent.getType());
    }

    public void clear() {
        setContent(null);
    }

    /**
     * Gets the widget that was previously set in {@link #setContent(Widget)}.
     *
     * @return the panel's current content widget
     */
    public Widget getContent() {
        return contentWrapper.getWidget();
    }

    /**
     * Gets the widget that is currently being used as a header.
     *
     * @return the widget currently being used as a header
     */
    public Widget getHeader() {
        return header.getWidget();
    }

    /**
     * Gets a {@link HasText} instance to provide access to the headers's text,
     * if the header widget does provide such access.
     *
     * @return a reference to the header widget if it implements {@link HasText}
     * , <code>null</code> otherwise
     */
    public HasText getHeaderTextAccessor() {
        Widget widget = header.getWidget();
        return (widget instanceof HasText) ? (HasText) widget : null;
    }

    public boolean isAnimationEnabled() {
        return isAnimationEnabled;
    }

    /**
     * Determines whether the panel is open.
     *
     * @return <code>true</code> if panel is in open state
     */
    public boolean isOpen() {
        return isOpen;
    }

    public Iterator<Widget> iterator() {
        return WidgetIterators.createWidgetIterator(this,
                new Widget[] { getContent() });
    }

    public boolean remove(Widget w) {
        if (w == getContent()) {
            setContent(null);
            return true;
        }
        return false;
    }

    /**
     * Removes an event handler from the panel.
     *
     * @param handler the handler to be removed
     * @deprecated Use the {@link HandlerRegistration#removeHandler} method on
     * the object returned by an add*Handler method instead
     */
    @Deprecated
    public void removeEventHandler(DisclosureHandler handler) {
    }

    public void setAnimationEnabled(boolean enable) {
        isAnimationEnabled = enable;
    }

    /**
     * Sets the content widget which can be opened and closed by this panel. If
     * there is a preexisting content widget, it will be detached.
     *
     * @param content the widget to be used as the content panel
     */
    public void setContent(Widget content) {
        final Widget currentContent = getContent();

        // Remove existing content widget.
        if (currentContent != null) {
            contentWrapper.setWidget(null);
            currentContent.removeStyleName(STYLENAME_CONTENT);
        }

        // Add new content widget if != null.
        if (content != null) {
            contentWrapper.setWidget(content);
            content.addStyleName(STYLENAME_CONTENT);
            setContentDisplay(false);
        }
    }

    /**
     * Sets the widget used as the header for the panel.
     *
     * @param headerWidget the widget to be used as the header
     */
    public void setHeader(Widget headerWidget) {
        header.setWidget(headerWidget);
    }

    /**
     * Changes the visible state of this <code>DisclosurePanel</code>.
     *
     * @param isOpen <code>true</code> to open the panel, <code>false</code> to
     * close
     */
    public void setOpen(boolean isOpen) {
        if (this.isOpen != isOpen) {
            this.isOpen = isOpen;
            setContentDisplay(true);
            fireEvent();
        }
    }

    /**
     * <b>Affected Elements:</b>
     * <ul>
     * <li>-header = the clickable header.</li>
     * </ul>
     *
     * @see UIObject#onEnsureDebugId(String)
     */
    @Override
    protected void onEnsureDebugId(String baseID) {
        super.onEnsureDebugId(baseID);
        header.ensureDebugId(baseID + "-header");
    }

    private void fireEvent() {
        if (isOpen) {
            OpenEvent.fire(this, this);
        } else {
            CloseEvent.fire(this, this);
        }
    }

    private void setContentDisplay(boolean animate) {
        if (isOpen) {
            removeStyleDependentName(STYLENAME_SUFFIX_CLOSED);
            addStyleDependentName(STYLENAME_SUFFIX_OPEN);
        } else {
            removeStyleDependentName(STYLENAME_SUFFIX_OPEN);
            addStyleDependentName(STYLENAME_SUFFIX_CLOSED);
        }

        if (getContent() != null) {
            if (contentAnimation == null) {
                contentAnimation = new ContentAnimation();
            }
            contentAnimation.setOpen(this, animate && isAnimationEnabled);
        }
    }
}
